/*
 * FloatSampleBuffer.java
 *
 *    This file is part of Tritonus: http://www.tritonus.org/
 */

/*
 *  Copyright (c) 2000-2006 by Florian Bomers <http://www.bomers.de>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 |<---            this code is formatted to fit into 80 columns             --->|
 */

package org.klab.packetcast.audio;

import javax.sound.sampled.AudioFormat;

/**
 * A class for small buffers of samples in linear, 32-bit floating point format.
 * <p>
 * It is supposed to be a replacement of the byte[] stream architecture of
 * JavaSound, especially for chains of AudioInputStreams. Ideally, all involved
 * AudioInputStreams handle reading into a FloatSampleBuffer.
 * <p>
 * Specifications:
 * <ol>
 * <li>Channels are separated, i.e. for stereo there are 2 float arrays with
 * the samples for the left and right channel
 * <li>All data is handled in samples, where one sample means one float value
 * in each channel
 * <li>All samples are normalized to the interval [-1.0...1.0]
 * </ol>
 * <p>
 * When a cascade of AudioInputStreams use FloatSampleBuffer for processing,
 * they may implement the interface FloatSampleInput. This signals that this
 * stream may provide float buffers for reading. The data is <i>not</i>
 * converted back to bytes, but stays in a single buffer that is passed from
 * stream to stream. For that serves the read(FloatSampleBuffer) method, which
 * is then used as replacement for the byte-based read functions of
 * AudioInputStream.<br>
 * However, backwards compatibility must always be retained, so even when an
 * AudioInputStream implements FloatSampleInput, it must work the same way when
 * any of the byte-based read methods is called.<br>
 * As an example, consider the following set-up:<br>
 * <ul>
 * <li>auAIS is an AudioInputStream (AIS) that reads from an AU file in 8bit
 * pcm at 8000Hz. It does not implement FloatSampleInput.
 * <li>pcmAIS1 is an AIS that reads from auAIS and converts the data to PCM
 * 16bit. This stream implements FloatSampleInput, i.e. it can generate float
 * audio data from the ulaw samples.
 * <li>pcmAIS2 reads from pcmAIS1 and adds a reverb. It operates entirely on
 * floating point samples.
 * <li>The method that reads from pcmAIS2 (i.e. AudioSystem.write) does not
 * handle floating point samples.
 * </ul>
 * So, what happens when a block of samples is read from pcmAIS2 ?
 * <ol>
 * <li>the read(byte[]) method of pcmAIS2 is called
 * <li>pcmAIS2 always operates on floating point samples, so it uses an own
 * instance of FloatSampleBuffer and initializes it with the number of samples
 * requested in the read(byte[]) method.
 * <li>It queries pcmAIS1 for the FloatSampleInput interface. As it implements
 * it, pcmAIS2 calls the read(FloatSampleBuffer) method of pcmAIS1.
 * <li>pcmAIS1 notes that its underlying stream does not support floats, so it
 * instantiates a byte buffer which can hold the number of samples of the
 * FloatSampleBuffer passed to it. It calls the read(byte[]) method of auAIS.
 * <li>auAIS fills the buffer with the bytes.
 * <li>pcmAIS1 calls the <code>initFromByteArray</code> method of the float
 * buffer to initialize it with the 8 bit data.
 * <li>Then pcmAIS1 processes the data: as the float buffer is normalized, it
 * does nothing with the buffer - and returns control to pcmAIS2. The
 * SampleSizeInBits field of the AudioFormat of pcmAIS1 defines that it should
 * be 16 bits.
 * <li>pcmAIS2 receives the filled buffer from pcmAIS1 and does its processing
 * on the buffer - it adds the reverb.
 * <li>As pcmAIS2's read(byte[]) method had been called, pcmAIS2 calls the
 * <code>convertToByteArray</code> method of the float buffer to fill the byte
 * buffer with the resulting samples.
 * </ol>
 * <p>
 * To summarize, here are some advantages when using a FloatSampleBuffer for
 * streaming:
 * <ul>
 * <li>no conversions from/to bytes need to be done during processing
 * <li>the sample size in bits is irrelevant - normalized range
 * <li>higher quality for processing
 * <li>separated channels (easy process/remove/add channels)
 * <li>potentially less copying of audio data, as processing the float samples
 * is generally done in-place. The same instance of a FloatSampleBuffer may be
 * used from the original data source to the final data sink.
 * </ul>
 * <p>
 * Simple benchmarks showed that the processing requirements for the conversion
 * to and from float is about the same as when converting it to shorts or ints
 * without dithering, and significantly higher with dithering. An own
 * implementation of a random number generator may improve this.
 * <p>
 * &quot;Lazy&quot; deletion of samples and channels:<br>
 * <ul>
 * <li>When the sample count is reduced, the arrays are not resized, but only
 * the member variable <code>sampleCount</code> is reduced. A subsequent
 * increase of the sample count (which will occur frequently), will check that
 * and eventually reuse the existing array.
 * <li>When a channel is deleted, it is not removed from memory but only
 * hidden. Subsequent insertions of a channel will check whether a hidden
 * channel can be reused.
 * </ul>
 * The lazy mechanism can save many array instantiation (and copy-) operations
 * for the sake of performance. All relevant methods exist in a second version
 * which allows explicitely to disable lazy deletion.
 * <p>
 * Use the <code>reset</code> functions to clear the memory and remove hidden
 * samples and channels.
 * <p>
 * Note that the lazy mechanism implies that the arrays returned from
 * <code>getChannel(int)</code> may have a greater size than getSampleCount().
 * Consequently, be sure to never rely on the length field of the sample arrays.
 * <p>
 * As an example, consider a chain of converters that all act on the same
 * instance of FloatSampleBuffer. Some converters may decrease the sample count
 * (e.g. sample rate converter) and delete channels (e.g. PCM2PCM converter).
 * So, processing of one block will decrease both. For the next block, all
 * starts from the beginning. With the lazy mechanism, all float arrays are only
 * created once for processing all blocks.<br>
 * Having lazy disabled would require for each chunk that is processed
 * <ol>
 * <li>new instantiation of all channel arrays at the converter chain beginning
 * as they have been either deleted or decreased in size during processing of
 * the previous chunk, and
 * <li>re-instantiation of all channel arrays for the reduction of the sample
 * count.
 * </ol>
 * <p>
 * Dithering:<br>
 * By default, this class uses dithering for reduction of sample width (e.g.
 * original data was 16bit, target data is 8bit). As dithering may be needed in
 * other cases (especially when the float samples are processed using DSP
 * algorithms), or it is preferred to switch it off, dithering can be
 * explicitely switched on or off with the method setDitherMode(int).<br>
 * For a discussion about dithering, see <a
 * href="http://www.iqsoft.com/IQSMagazine/BobsSoapbox/Dithering.htm"> here</a>
 * and <a href="http://www.iqsoft.com/IQSMagazine/BobsSoapbox/Dithering2.htm">
 * here</a>.
 * 
 * @author Florian Bomers
 */

public class FloatSampleBuffer {

      /** Whether the functions without lazy parameter are lazy or not. */
      private static final boolean LAZY_DEFAULT = true;

      // one float array for each channel
      private Object[] channels = new Object[2];
      private int sampleCount = 0;
      private int channelCount = 0;
      private float sampleRate = 0;
      private int originalFormatType = 0;

      /**
       * Constant for setDitherMode: dithering will be enabled if sample size is
       * decreased
       */
      public static final int DITHER_MODE_AUTOMATIC = 0;
      /** Constant for setDitherMode: dithering will be done */
      public static final int DITHER_MODE_ON = 1;
      /** Constant for setDitherMode: dithering will not be done */
      public static final int DITHER_MODE_OFF = 2;

      private float ditherBits = FloatSampleTools.DEFAULT_DITHER_BITS;

      // e.g. the sample rate converter may want to force dithering
      private int ditherMode = DITHER_MODE_AUTOMATIC;

      // ////////////////////////////// initialization //////////////////////

      /**
       * Create an instance with initially no channels.
       */
      public FloatSampleBuffer() {
            this(0, 0, 1);
      }

      /**
       * Create an empty FloatSampleBuffer with the specified number of channels,
       * samples, and the specified sample rate.
       */
      public FloatSampleBuffer(int channelCount, int sampleCount, float sampleRate) {
            init(channelCount, sampleCount, sampleRate, LAZY_DEFAULT);
      }

      /**
       * Creates a new instance of FloatSampleBuffer and initializes it with audio
       * data given in the interleaved byte array <code>buffer</code>.
       */
      public FloatSampleBuffer(byte[] buffer, int offset, int byteCount,
                  AudioFormat format) {
            this(format.getChannels(), byteCount
                        / (format.getSampleSizeInBits() / 8 * format.getChannels()),
                        format.getSampleRate());
            initFromByteArray(buffer, offset, byteCount, format);
      }

      /**
       * Initialize this sample buffer to have the specified channels, sample
       * count, and sample rate. If LAZY_DEFAULT is true, as much as possible will
       * existing arrays be reused. Otherwise, any hidden channels are freed.
       * 
       * @param newChannelCount
       * @param newSampleCount
       * @param newSampleRate
       * @throws IllegalArgumentException if newChannelCount or newSampleCount are
       *             negative, or newSampleRate is not positive.
       */
      public void init(int newChannelCount, int newSampleCount,
                  float newSampleRate) {
            init(newChannelCount, newSampleCount, newSampleRate, LAZY_DEFAULT);
      }

      /**
       * Initialize this sample buffer to have the specified channels, sample
       * count, and sample rate. If lazy is true, as much as possible will
       * existing arrays be reused. Otherwise, any hidden channels are freed.
       * 
       * @param newChannelCount
       * @param newSampleCount
       * @param newSampleRate
       * @param lazy
       * @throws IllegalArgumentException if newChannelCount or newSampleCount are
       *             negative, or newSampleRate is not positive.
       */
      public void init(int newChannelCount, int newSampleCount,
                  float newSampleRate, boolean lazy) {
            if (newChannelCount < 0 || newSampleCount < 0 || newSampleRate <= 0.0f) {
                  throw new IllegalArgumentException(
                              "invalid parameters in initialization of FloatSampleBuffer.");
            }
            setSampleRate(newSampleRate);
            if (this.sampleCount != newSampleCount
                        || this.channelCount != newChannelCount) {
                  createChannels(newChannelCount, newSampleCount, lazy);
            }
      }
      
      /**
       * Verify that the specified AudioFormat can be converted to and from.
       * If the format is not supported, an IllegalArgumentException is thrown.
       * @throws IllegalArgumentException if the format is not supported
       */
      public static void checkFormatSupported(AudioFormat format) {
            FloatSampleTools.getFormatType(format);
      }

      /**
       * Grow the channels array to allow at least channelCount elements. If
       * !lazy, then channels will be resized to be exactly channelCount elements.
       * The new elements will be null.
       * 
       * @param newChannelCount
       * @param lazy
       */
      private final void grow(int newChannelCount, boolean lazy) {
            if (channels.length < newChannelCount || !lazy) {
                  Object[] newChannels = new Object[newChannelCount];
                  System.arraycopy(channels, 0, newChannels, 0,
                              (channelCount < newChannelCount) ? channelCount
                                          : newChannelCount);
                  this.channels = newChannels;
            }
      }

      private final void createChannels(int newChannelCount, int newSampleCount,
                  boolean lazy) {
            // shortcut
            if (lazy && newChannelCount <= channelCount
                        && newSampleCount <= this.sampleCount) {
                  setSampleCountImpl(newSampleCount);
                  setChannelCountImpl(newChannelCount);
                  return;
            }
            setSampleCountImpl(newSampleCount);
            // grow the array, if necessary. Intentionally lazy here!
            grow(newChannelCount, true);
            // lazy delete of all channels. Intentionally lazy !
            setChannelCountImpl(0);
            for (int ch = 0; ch < newChannelCount; ch++) {
                  insertChannel(ch, false, lazy);
            }
            // if not lazy, remove hidden channels
            grow(newChannelCount, lazy);
      }

      /**
       * Resets this buffer with the audio data specified in the arguments. This
       * FloatSampleBuffer's sample count will be set to
       * <code>byteCount / format.getFrameSize()</code>. If LAZY_DEFAULT is
       * true, it will use lazy deletion.
       * 
       * @throws IllegalArgumentException
       */
      public void initFromByteArray(byte[] buffer, int offset, int byteCount,
                  AudioFormat format) {
            initFromByteArray(buffer, offset, byteCount, format, LAZY_DEFAULT);
      }

      /**
       * Resets this buffer with the audio data specified in the arguments. This
       * FloatSampleBuffer's sample count will be set to
       * <code>byteCount / format.getFrameSize()</code>.
       * 
       * @param lazy if true, then existing channels will be tried to be re-used
       *            to minimize garbage collection.
       * @throws IllegalArgumentException
       */
      public void initFromByteArray(byte[] buffer, int offset, int byteCount,
                  AudioFormat format, boolean lazy) {
            if (offset + byteCount > buffer.length) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer.initFromByteArray: buffer too small.");
            }

            int thisSampleCount = byteCount / format.getFrameSize();
            init(format.getChannels(), thisSampleCount, format.getSampleRate(),
                        lazy);

            // save format for automatic dithering mode
            originalFormatType = FloatSampleTools.getFormatType(format);

            FloatSampleTools.byte2float(buffer, offset, channels, 0, sampleCount,
                        format);
      }

      /**
       * Resets this sample buffer with the data in <code>source</code>.
       */
      public void initFromFloatSampleBuffer(FloatSampleBuffer source) {
            init(source.getChannelCount(), source.getSampleCount(),
                        source.getSampleRate());
            for (int ch = 0; ch < getChannelCount(); ch++) {
                  System.arraycopy(source.getChannel(ch), 0, getChannel(ch), 0,
                              sampleCount);
            }
      }

      /**
       * Deletes all channels, frees memory... This also removes hidden channels
       * by lazy remove.
       */
      public void reset() {
            init(0, 0, 1, false);
      }

      /**
       * Destroys any existing data and creates new channels. It also destroys
       * lazy removed channels and samples. Channels will not be silenced, though.
       */
      public void reset(int newChannels, int newSampleCount, float newSampleRate) {
            init(newChannels, newSampleCount, newSampleRate, false);
      }

      // //////////////////////// conversion back to bytes ///////////////////

      /**
       * @return the required size of the buffer for calling
       *         convertToByteArray(..) is called
       */
      public int getByteArrayBufferSize(AudioFormat format) {
            return getByteArrayBufferSize(format, getSampleCount());
      }

      /**
       * @param lenInSamples how many samples to be considered
       * @return the required size of the buffer for the given number of samples
       *         for calling convertToByteArray(..)
       */
      public int getByteArrayBufferSize(AudioFormat format, int lenInSamples) {
            // make sure this format is supported
            checkFormatSupported(format);
            return format.getFrameSize() * lenInSamples;
      }

      /**
       * Writes this sample buffer's audio data to <code>buffer</code> as an
       * interleaved byte array. <code>buffer</code> must be large enough to
       * hold all data.
       * 
       * @throws IllegalArgumentException when buffer is too small or
       *             <code>format</code> doesn't match
       * @return number of bytes written to <code>buffer</code>
       */
      public int convertToByteArray(byte[] buffer, int offset, AudioFormat format) {
            return convertToByteArray(0, getSampleCount(), buffer, offset, format);
      }

      // cache for performance
      private AudioFormat lastConvertToByteArrayFormat = null;
      private int lastConvertToByteArrayFormatCode = 0;

      /**
       * Writes this sample buffer's audio data to <code>buffer</code> as an
       * interleaved byte array. <code>buffer</code> must be large enough to
       * hold all data.
       * 
       * @param readOffset the sample offset from where samples are read from this
       *            FloatSampleBuffer
       * @param lenInSamples how many samples are converted
       * @param buffer the byte buffer written to
       * @param writeOffset the byte offset in buffer
       * @throws IllegalArgumentException when buffer is too small or
       *             <code>format</code> doesn't match
       * @return number of bytes written to <code>buffer</code>
       */
      public int convertToByteArray(int readOffset, int lenInSamples,
                  byte[] buffer, int writeOffset, AudioFormat format) {
            int byteCount = format.getFrameSize() * lenInSamples;
            if (writeOffset + byteCount > buffer.length) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer.convertToByteArray: buffer too small.");
            }
            if (format != lastConvertToByteArrayFormat) {
                  if (format.getSampleRate() != getSampleRate()) {
                        throw new IllegalArgumentException(
                                    "FloatSampleBuffer.convertToByteArray: different samplerates.");
                  }
                  if (format.getChannels() != getChannelCount()) {
                        throw new IllegalArgumentException(
                                    "FloatSampleBuffer.convertToByteArray: different channel count.");
                  }
                  lastConvertToByteArrayFormat = format;
                  lastConvertToByteArrayFormatCode = FloatSampleTools.getFormatType(format);
            }
            FloatSampleTools.float2byte(channels, readOffset, buffer, writeOffset,
                        lenInSamples, lastConvertToByteArrayFormatCode,
                        format.getChannels(), format.getFrameSize(),
                        getConvertDitherBits(lastConvertToByteArrayFormatCode));

            return byteCount;
      }

      /**
       * Creates a new byte[] buffer, fills it with the audio data, and returns
       * it.
       * 
       * @throws IllegalArgumentException when sample rate or channels do not
       *             match
       * @see #convertToByteArray(byte[], int, AudioFormat)
       */
      public byte[] convertToByteArray(AudioFormat format) {
            // throws exception when sampleRate doesn't match
            // creates a new byte[] buffer and returns it
            byte[] res = new byte[getByteArrayBufferSize(format)];
            convertToByteArray(res, 0, format);
            return res;
      }

      // ////////////////////////////// actions /////////////////////////////////

      /**
       * Resizes this buffer.
       * <p>
       * If <code>keepOldSamples</code> is true, as much as possible samples are
       * retained. If the buffer is enlarged, silence is added at the end. If
       * <code>keepOldSamples</code> is false, existing samples may get
       * discarded, the buffer may then contain random samples.
       */
      public void changeSampleCount(int newSampleCount, boolean keepOldSamples) {
            int oldSampleCount = getSampleCount();

            // shortcut: if we just make this buffer smaller, just set new
            // sampleCount
            if (oldSampleCount >= newSampleCount) {
                  setSampleCountImpl(newSampleCount);
                  return;
            }
            // shortcut for one or 2 channels
            if (channelCount == 1 || channelCount == 2) {
                  float[] ch = getChannel(0);
                  if (ch.length < newSampleCount) {
                        float[] newCh = new float[newSampleCount];
                        if (keepOldSamples && oldSampleCount > 0) {
                              // copy old samples
                              System.arraycopy(ch, 0, newCh, 0, oldSampleCount);
                        }
                        channels[0] = newCh;
                  } else if (keepOldSamples) {
                        // silence out excess samples (according to the specification)
                        for (int i = oldSampleCount; i < newSampleCount; i++) {
                              ch[i] = 0.0f;
                        }
                  }
                  if (channelCount == 2) {
                        ch = getChannel(1);
                        if (ch.length < newSampleCount) {
                              float[] newCh = new float[newSampleCount];
                              if (keepOldSamples && oldSampleCount > 0) {
                                    // copy old samples
                                    System.arraycopy(ch, 0, newCh, 0, oldSampleCount);
                              }
                              channels[1] = newCh;
                        } else if (keepOldSamples) {
                              // silence out excess samples (according to the
                              // specification)
                              for (int i = oldSampleCount; i < newSampleCount; i++) {
                                    ch[i] = 0.0f;
                              }
                        }
                  }
                  setSampleCountImpl(newSampleCount);
                  return;
            }

            Object[] oldChannels = null;
            if (keepOldSamples) {
                  oldChannels = getAllChannels();
            }
            init(getChannelCount(), newSampleCount, getSampleRate());
            if (keepOldSamples) {
                  // copy old channels and eventually silence out new samples
                  int copyCount = newSampleCount < oldSampleCount ? newSampleCount
                              : oldSampleCount;
                  for (int ch = 0; ch < this.channelCount; ch++) {
                        float[] oldSamples = (float[]) oldChannels[ch];
                        float[] newSamples = (float[]) channels[ch];
                        if (oldSamples != newSamples) {
                              // if this sample array was not object of lazy delete
                              System.arraycopy(oldSamples, 0, newSamples, 0, copyCount);
                        }
                        if (oldSampleCount < newSampleCount) {
                              // silence out new samples
                              for (int i = oldSampleCount; i < newSampleCount; i++) {
                                    newSamples[i] = 0.0f;
                              }
                        }
                  }
            }
      }

      /**
       * Silence the entire audio buffer.
       */
      public void makeSilence() {
            makeSilence(0, getSampleCount());
      }

      /**
       * Silence the entire buffer in the specified range on all channels.
       */
      public void makeSilence(int offset, int count) {
            if (offset < 0 || (count + offset) > getSampleCount() || count < 0) {
                  throw new IllegalArgumentException(
                              "offset and/or sampleCount out of bounds");
            }
            // silence all channels
            int localChannelCount = getChannelCount();
            for (int ch = 0; ch < localChannelCount; ch++) {
                  makeSilence(getChannel(ch), offset, count);
            }
      }

      /**
       * Silence the specified channel
       */
      public void makeSilence(int channel) {
            makeSilence(channel, 0, getSampleCount());
      }

      /**
       * Silence the specified channel in the specified range
       */
      public void makeSilence(int channel, int offset, int count) {
            if (offset < 0 || (count + offset) > getSampleCount() || count < 0) {
                  throw new IllegalArgumentException(
                              "offset and/or sampleCount out of bounds");
            }
            makeSilence(getChannel(channel), offset, count);
      }

      private void makeSilence(float[] samples, int offset, int count) {
            count += offset;
            for (int i = offset; i < count; i++) {
                  samples[i] = 0.0f;
            }
      }

      public void addChannel(boolean silent) {
            // creates new, silent channel
            insertChannel(getChannelCount(), silent);
      }

      /**
       * Insert a (silent) channel at position <code>index</code>. If
       * LAZY_DEFAULT is true, this is done lazily.
       */
      public void insertChannel(int index, boolean silent) {
            insertChannel(index, silent, LAZY_DEFAULT);
      }

      /**
       * Inserts a channel at position <code>index</code>.
       * <p>
       * If <code>silent</code> is true, the new channel will be silent.
       * Otherwise it will contain random data.
       * <p>
       * If <code>lazy</code> is true, hidden channels which have at least
       * getSampleCount() elements will be examined for reusage as inserted
       * channel.<br>
       * If <code>lazy</code> is false, still hidden channels are reused, but it
       * is assured that the inserted channel has exactly getSampleCount()
       * elements, thus not wasting memory.
       */
      public void insertChannel(int index, boolean silent, boolean lazy) {
            // first grow the array of channels, if necessary. Intentionally lazy
            grow(this.channelCount + 1, true);
            int physSize = channels.length;
            int virtSize = this.channelCount;
            float[] newChannel = null;
            if (physSize > virtSize) {
                  // there are hidden channels. Try to use one.
                  for (int ch = virtSize; ch < physSize; ch++) {
                        float[] thisChannel = (float[]) channels[ch];
                        if (thisChannel != null
                                    && ((lazy && thisChannel.length >= getSampleCount()) || (!lazy && thisChannel.length == getSampleCount()))) {
                              // we found a matching channel. Use it !
                              newChannel = thisChannel;
                              channels[ch] = null;
                              break;
                        }
                  }
            }
            if (newChannel == null) {
                  newChannel = new float[getSampleCount()];
            }
            // move channels after index
            for (int i = index; i < virtSize; i++) {
                  channels[i + 1] = channels[i];
            }
            channels[index] = newChannel;
            setChannelCountImpl(this.channelCount + 1);
            if (silent) {
                  makeSilence(index);
            }
            // if not lazy, remove old channels
            grow(this.channelCount, lazy);
      }

      /** performs a lazy remove of the channel */
      public void removeChannel(int channel) {
            removeChannel(channel, LAZY_DEFAULT);
      }

      /**
       * Removes a channel. If lazy is true, the channel is not physically
       * removed, but only hidden. These hidden channels are reused by subsequent
       * calls to addChannel or insertChannel.
       */
      public void removeChannel(int channel, boolean lazy) {
            float[] toBeDeleted = (float[]) channels[channel];
            // move all channels after it
            for (int i = channel; i < this.channelCount - 1; i++) {
                  channels[i] = channels[i + 1];
            }
            if (!lazy) {
                  grow(this.channelCount - 1, true);
            } else {
                  // if not already, insert this channel at the end
                  channels[this.channelCount - 1] = toBeDeleted;
            }
            setChannelCountImpl(channelCount - 1);
      }

      /**
       * both source and target channel have to exist. targetChannel will be
       * overwritten
       */
      public void copyChannel(int sourceChannel, int targetChannel) {
            float[] source = getChannel(sourceChannel);
            float[] target = getChannel(targetChannel);
            System.arraycopy(source, 0, target, 0, getSampleCount());
      }

      /**
       * Copies data inside all channel. When the 2 regions overlap, the behavior
       * is not specified.
       */
      public void copy(int sourceIndex, int destIndex, int length) {
            int count = getChannelCount();
            for (int i = 0; i < count; i++) {
                  copy(i, sourceIndex, destIndex, length);
            }
      }

      /**
       * Copies data inside a channel. When the 2 regions overlap, the behavior is
       * not specified.
       */
      public void copy(int channel, int sourceIndex, int destIndex, int length) {
            float[] data = getChannel(channel);
            int bufferCount = getSampleCount();
            if (sourceIndex + length > bufferCount
                        || destIndex + length > bufferCount || sourceIndex < 0
                        || destIndex < 0 || length < 0) {
                  throw new IndexOutOfBoundsException("parameters exceed buffer size");
            }
            System.arraycopy(data, sourceIndex, data, destIndex, length);
      }

      /**
       * Mix up of 1 channel to n channels.<br>
       * It copies the first channel to all newly created channels.
       * 
       * @param targetChannelCount the number of channels that this sample buffer
       *            will have after expanding. NOT the number of channels to add !
       * @exception IllegalArgumentException if this buffer does not have one
       *                channel before calling this method.
       */
      public void expandChannel(int targetChannelCount) {
            // even more sanity...
            if (getChannelCount() != 1) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer: can only expand channels for mono signals.");
            }
            for (int ch = 1; ch < targetChannelCount; ch++) {
                  addChannel(false);
                  copyChannel(0, ch);
            }
      }

      /**
       * Mix down of n channels to one channel.<br>
       * It uses a simple mixdown: all other channels are added to first channel.<br>
       * The volume is NOT lowered ! Be aware, this might cause clipping when
       * converting back to integer samples.
       */
      public void mixDownChannels() {
            float[] firstChannel = getChannel(0);
            int localSampleCount = getSampleCount();
            for (int ch = getChannelCount() - 1; ch > 0; ch--) {
                  float[] thisChannel = getChannel(ch);
                  for (int i = 0; i < localSampleCount; i++) {
                        firstChannel[i] += thisChannel[i];
                  }
                  removeChannel(ch);
            }
      }

      /**
       * Mixes <code>buffer</code> to this buffer by adding all samples. At
       * most, <code>source</code>'s number of samples, number of channels are
       * mixed. None of the sample count, channel count or sample rate of either
       * buffer are changed. In particular, the caller needs to assure that the
       * sample rate of the buffers match.
       * 
       * @param source the buffer to be mixed to this buffer
       */
      public void mix(FloatSampleBuffer source) {
            int count = getSampleCount();
            if (count > source.getSampleCount()) {
                  count = source.getSampleCount();
            }
            int localChannelCount = getChannelCount();
            if (localChannelCount > source.getChannelCount()) {
                  localChannelCount = source.getChannelCount();
            }
            for (int ch = 0; ch < localChannelCount; ch++) {
                  float[] thisChannel = getChannel(ch);
                  float[] otherChannel = source.getChannel(ch);
                  for (int i = 0; i < count; i++) {
                        thisChannel[i] += otherChannel[i];
                  }
            }
      }

      /**
       * Copies the contents of this buffer to the destination buffer at the
       * destOffset. At most, <code>dest</code>'s number of samples, number of
       * channels are copied. None of the sample count, channel count or sample
       * rate of either buffer are changed. In particular, the caller needs to
       * assure that the sample rate of the buffers match.
       * 
       * @param dest the buffer to write to
       * @param destOffset the position in <code>dest</code> where to start
       *            writing the samples of this buffer
       * @param count the number of samples to be copied
       */
      public void copyTo(FloatSampleBuffer dest, int destOffset, int count) {
            if (count > getSampleCount()) {
                  count = getSampleCount();
            }
            if (count + destOffset > dest.getSampleCount()) {
                  count = dest.getSampleCount() - destOffset;
            }
            int localChannelCount = getChannelCount();
            if (localChannelCount > dest.getChannelCount()) {
                  localChannelCount = dest.getChannelCount();
            }
            for (int ch = 0; ch < localChannelCount; ch++) {
                  System.arraycopy(getChannel(ch), 0, dest.getChannel(ch),
                              destOffset, count);
            }
      }

      /**
       * Initializes audio data from the provided byte array. The float samples
       * are written at <code>destOffset</code>. This FloatSampleBuffer must be
       * big enough to accomodate the samples.
       * <p>
       * <code>srcBuffer</code> is read from index <code>srcOffset</code> to
       * <code>(srcOffset + (lengthInSamples * format.getFrameSize()))</code.
       *
       * @param input the input buffer in interleaved audio data
       * @param inByteOffset the offset in <code>input</code>
       * @param format input buffer's audio format
       * @param floatOffset the offset where to write the float samples
       * @param frameCount number of samples to write to this sample buffer
       */
      public void setSamplesFromBytes(byte[] input, int inByteOffset,
                  AudioFormat format, int floatOffset, int frameCount) {
            if (floatOffset < 0 || frameCount < 0 || inByteOffset < 0) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer.setSamplesFromBytes: negative inByteOffset, floatOffset, or frameCount");
            }
            if (inByteOffset + (frameCount * format.getFrameSize()) > input.length) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer.setSamplesFromBytes: input buffer too small.");
            }
            if (floatOffset + frameCount > getSampleCount()) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer.setSamplesFromBytes: frameCount too large");
            }
            FloatSampleTools.byte2float(input, inByteOffset, channels, floatOffset,
                        frameCount, format, false);
      }

      // ////////////////////////////// properties /////////////////////////////

      public int getChannelCount() {
            return channelCount;
      }

      public int getSampleCount() {
            return sampleCount;
      }

      public float getSampleRate() {
            return sampleRate;
      }

      /**
       * internal setter for channel count, just change the variable. From
       * outside, use addChannel, insertChannel, removeChannel
       */
      protected void setChannelCountImpl(int newChannelCount) {
            if (channelCount != newChannelCount) {
                  channelCount = newChannelCount;
                  // remove cache
                  this.lastConvertToByteArrayFormat = null;
            }
      }

      /**
       * internal setter for sample count, just change the variable. From outside,
       * use changeSampleCount
       */
      protected void setSampleCountImpl(int newSampleCount) {
            if (sampleCount != newSampleCount) {
                  sampleCount = newSampleCount;
            }
      }

      /**
       * Alias for changeSampleCount
       * 
       * @param newSampleCount the new number of samples for this buffer
       * @param keepOldSamples if true, the new buffer will keep the current
       *            samples in the arrays
       * @see #changeSampleCount(int, boolean)ngeSampleCount(int, boolean)
       */
      public void setSampleCount(int newSampleCount, boolean keepOldSamples) {
            changeSampleCount(newSampleCount, keepOldSamples);
      }

      /**
       * Sets the sample rate of this buffer. NOTE: no conversion is done. The
       * samples are only re-interpreted.
       */
      public void setSampleRate(float sampleRate) {
            if (sampleRate <= 0) {
                  throw new IllegalArgumentException(
                              "Invalid samplerate for FloatSampleBuffer.");
            }
            if (this.sampleRate != sampleRate) {
                  this.sampleRate = sampleRate;
                  // remove cache
                  lastConvertToByteArrayFormat = null;
            }
      }

      /**
       * NOTE: the returned array may be larger than sampleCount. So in any case,
       * sampleCount is to be respected.
       */
      public float[] getChannel(int channel) {
            if (channel >= this.channelCount) {
                  throw new IllegalArgumentException(
                              "FloatSampleBuffer: invalid channel number.");
            }
            return (float[]) channels[channel];
      }

      /**
       * @return all channels as array
       */
      public Object[] getAllChannels() {
            Object[] res = new Object[getChannelCount()];
            for (int ch = 0; ch < getChannelCount(); ch++) {
                  res[ch] = getChannel(ch);
            }
            return res;
      }

      /**
       * Set the number of bits for dithering. Typically, a value between 0.2 and
       * 0.9 gives best results.
       * <p>
       * Note: this value is only used, when dithering is actually performed.
       */
      public void setDitherBits(float ditherBits) {
            if (ditherBits <= 0) {
                  throw new IllegalArgumentException(
                              "DitherBits must be greater than 0");
            }
            this.ditherBits = ditherBits;
      }

      public float getDitherBits() {
            return ditherBits;
      }

      /**
       * Sets the mode for dithering. This can be one of:
       * <ul>
       * <li>DITHER_MODE_AUTOMATIC: it is decided automatically, whether
       * dithering is necessary - in general when sample size is decreased.
       * <li>DITHER_MODE_ON: dithering will be forced
       * <li>DITHER_MODE_OFF: dithering will not be done.
       * </ul>
       */
      public void setDitherMode(int mode) {
            if (mode != DITHER_MODE_AUTOMATIC && mode != DITHER_MODE_ON
                        && mode != DITHER_MODE_OFF) {
                  throw new IllegalArgumentException("Illegal DitherMode");
            }
            this.ditherMode = mode;
      }

      public int getDitherMode() {
            return ditherMode;
      }

      /**
       * @return the ditherBits parameter for the float2byte functions
       */
      protected float getConvertDitherBits(int newFormatType) {
            // let's see whether dithering is necessary
            boolean doDither = false;
            switch (ditherMode) {
            case DITHER_MODE_AUTOMATIC:
                  doDither = (originalFormatType & FloatSampleTools.F_SAMPLE_WIDTH_MASK) > (newFormatType & FloatSampleTools.F_SAMPLE_WIDTH_MASK);
                  break;
            case DITHER_MODE_ON:
                  doDither = true;
                  break;
            case DITHER_MODE_OFF:
                  doDither = false;
                  break;
            }
            return doDither ? ditherBits : 0.0f;
      }
}
